/**
 * 
 */
package com.google.code.rookery;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;

import com.google.code.rookery.converter.InstanceConverter;

/**
 * @author Xerox
 *
 */
public class FactoryMethodMapping implements Comparable<FactoryMethodMapping>{
  private static final String   GET     = "get";
  private static final String   IS      = "is";
  private static final String   SET     = "set";

  private static Logger _log = Logger.getLogger(FactoryMethodMapping.class);

  private Method            factoryMethod;
  private List<Mapping>     mappings = new ArrayList<Mapping>();

  /**
   * Find all of the properties in a class tagged with the FactoryProperty annotation
   * @param clazz the class being explored
   * @return an array of the properties
   */
  public static Field[] findFactoryPropertyFields(Class<?> clazz){
    //holder for the fields
    List<Field> fields = new ArrayList<Field>();

    //find all of the properties in this feeder class
    for(Field field:clazz.getDeclaredFields()){
      //check the annotations on that field
      for(Annotation annotation:field.getAnnotations()){
        //found an annotation, see if it's one of our properties
        if (annotation instanceof FactoryProperty) {
          //it is one of our properties, add it
          fields.add(field);
        }
      }
    }

    //convert to array
    Field[] ret = new Field[fields.size()];
    ret = fields.toArray(ret);
    return ret;
  }

  /**
   * Find all of the properties in a class tagged with the FactoryProperty annotation
   * @param clazz the class being explored
   * @return an array of the properties
   */
  public static Method[] findFactoryPropertyGetters(Class<?> clazz){
    //holder for the getters
    List<Method> methods = new ArrayList<Method>();

    //find all of the properties in this feeder class
    for(Method method:clazz.getMethods()){
      //check the annotations on that method
      for(Annotation annotation:method.getAnnotations()){
        //found an annotation, see if it's one of our properties
        if (annotation instanceof FactoryProperty) {
          //it is one of our properties, make sure it's a getter
          String name = method.getName();
          if(name.startsWith(GET)||name.startsWith(IS)){
            if(method.getParameterTypes().length!=0){
              throw new IllegalArgumentException("getters must not have any parameters:" + name);
            }
            methods.add(method);
          }
          else{
            throw new IllegalArgumentException("FactoryProperty must define a getter instead of:" + name);
          }
        }
      }
    }

    //convert to array
    Method[] ret = new Method[methods.size()];
    ret = methods.toArray(ret);
    return ret;
  }

  /**
   * Find the method with a unique name in the class method. Note that more than one
   * method with the same name results in IllegalStateException
   * @param clazz the class being explored
   * @param methodName the method name sought
   * @return the method or null if no match
   */
  public static Method findMethodByName(Class<?> clazz, String methodName){
    Method ret = null;

    //loop through the methods on the composite class to find the setter
    for(Method method:clazz.getMethods()){
      //see if there's a name match
      if(methodName.equals(method.getName())){
        if(ret==null){
          //found it, store it.
          ret = method;
        }
        else{
          //make sure there's not more than one
          throw new IllegalStateException("Multiple instances found of setter method name:" + methodName);
        }
      }
    }

    return ret;
  }

  /**
   * Find the getter in a class for the property name
   * @param clazz the java bean class
   * @param propName the name of the property being matched to the setter
   * @return the setter method or null if not found
   */
  public static Method findGetter(Class<?> clazz, String propName){
    //extract the name of the property and convert to a setter
    String getterName = propNameToSetter(propName).replace(SET, GET);
    //test for get
    Method getter = findMethodByName(clazz, getterName);
    if(getter==null){
      //try is
      getterName = getterName.replace(GET, IS);
      getter = findMethodByName(clazz, getterName);
    }
    //return result
    return getter;
  }

  /**
   * Find the setter in a class for the property name
   * @param clazz the java bean class
   * @param propName the name of the property being matched to the setter
   * @return the setter method or null if not found
   */
  public static Method findSetter(Class<?> clazz, String propName){
    //extract the name of the property and convert to a setter
    String setterName = propNameToSetter(propName);
    Method setter = findMethodByName(clazz, setterName);
    //get the method
    return setter;
  }

  /**
   * Build a unique key for the provided method. Will match getKey()
   * @return a unique key as a string
   */
  public static String getKey(Method method){
    StringBuilder sb = new StringBuilder();

    sb.append(method.getReturnType().getName());
    for(Class<?> clazz:method.getParameterTypes()){
      sb.append(' ');
      sb.append(clazz.getName());
    }

    return sb.toString();
  }

  /**
   * Retrieve the list of mappings built
   * @return the list of mappings
   */
  protected List<Mapping> getMappings(){
    return mappings;
  }

  /**
   * Convert a getter name to the name of it's property
   * @param getterName the name of a getter like getName
   * @return the property name like name
   */
  public static String getterNameToPropertyName(String getterName){
    String name;
    if(getterName.startsWith(GET)){
      name = getterName.substring(3);
    }
    else if(getterName.startsWith(IS)){
      name = getterName.substring(2);
    }
    else{
      throw new IllegalArgumentException("Getter name is not a getter name:" + getterName);
    }

    char firstChar = Character.toLowerCase(name.charAt(0));
    String propName = Character.toString(firstChar) + name.substring(1);
    return propName;
  }

  /**
   * Convert the name of a java bean property to a setter
   * @param propName the name of the property
   * @return the name of the expected setter method for that property
   */
  public static String propNameToSetter(String propName){
    //change the first character of the property name to upper case
    char firstChar = Character.toUpperCase(propName.charAt(0));
    //now build the setter
    StringBuilder sb = new StringBuilder(SET);
    //apply the converted first char
    sb.append(firstChar);
    //apply the rest of the property name
    sb.append(propName.substring(1));

    return sb.toString();
  }

  /**
   * The mapping used by a factory method to convert properties of a group of
   * java beans to a particular java bean
   * @param method the factory method being generated by this mapping
   */
  public FactoryMethodMapping(Method method) {
    this.factoryMethod = method;
    // cache the class we're creating in the factory method
    Class<?> compositeClass = method.getReturnType();
    // loop through the parameters and process each of the feeder classes
    // for the factory
    for (Class<?> feederClass : method.getParameterTypes()) {
      // loop through each of the fields
      for (Field feederField : findFactoryPropertyFields(feederClass)) {
        // find the factory property
        FactoryProperty prop = null;
        for (Annotation annotation : feederField.getAnnotations()) {
          if (annotation instanceof FactoryProperty) {
            prop = (FactoryProperty) annotation;
            break;
          }
        }
        // find the setter for the named property
        Method setter = findSetter(compositeClass, prop.name());
        if (setter == null) {
          // setter not found, log it
          _log.debug("Setter not found for property:" + prop.name());
        }
        else {
          Mapping mapping = buildMapping(feederClass, feederField.getName(), compositeClass, setter, prop);
          if(mapping!=null) mappings.add(mapping);
        }
      }

      // loop through each of the getters
      for (Method getter : findFactoryPropertyGetters(feederClass)) {
        // find the factory property
        FactoryProperty prop = null;
        for (Annotation annotation : getter.getAnnotations()) {
          if (annotation instanceof FactoryProperty) {
            prop = (FactoryProperty) annotation;
            break;
          }
        }
        // find the setter for the named property
        Method setter = findSetter(compositeClass, prop.name());
        if (setter == null) {
          // setter not found, log it
          _log.debug("Setter not found for property:" + prop.name());
        }
        else {
          String getterPropertyName = getterNameToPropertyName(getter.getName());
          Mapping mapping = buildMapping(feederClass, getterPropertyName, compositeClass, setter, prop);
          if(mapping!=null) mappings.add(mapping);
        }
      }
    }
  }

  /**
   * Build the mapping for a setter and a property
   * @param fromClass the class with the property being read
   * @param propName the name of the property
   * @param toClass the class the value will be copied to
   * @param setter the setter that will receive the properties value
   * @param factProp the factory property for the fromClass
   * @return the mapping or null if not able to be built.
   */
  protected Mapping buildMapping(Class<?> fromClass, String propName, Class<?> toClass, Method setter, FactoryProperty factProp) {
    Class<?>[] parmTypes = setter.getParameterTypes();
    if (parmTypes.length != 1) {
      throw new IllegalStateException("Setter method without a single parameter:" + setter.getName());
    }
    Class<?> setterType = parmTypes[0];
    Method getter = findGetter(fromClass, propName);
    Mapping mapping = null;
    if (getter == null) {
      _log.warn("Getter not found for property:" + propName);
    }
    else {
      Class<?> getterType = getter.getReturnType();
      InstanceConverter converter;
      Class<? extends InstanceConverter> converterClass = factProp.converter();
      if(converterClass.equals(InstanceConverter.class)){
        //no converter defined, see if one's needed
        if (getterType.equals(setterType)) {
          //no converter needed
          converter = null;
        }
        else{
          //look for one
          converter = lookForConverter(getterType, setterType);
        }
      }
      else{
        try {
          converter = converterClass.newInstance();
        }
        catch (InstantiationException ie) {
          throw new RuntimeException("Error constructing converter", ie);
        }
        catch (IllegalAccessException iae) {
          throw new RuntimeException("Error constructing converter", iae);
        }
      }
      mapping = new Mapping(fromClass, getter, toClass, setter, converter);
    }
    return mapping;
  }

  /**
   * Look for a converter
   * @param getterType the type of the getter method
   * @param setterType the type of the setter method
   * @return the converter or null if there isn't one
   */
  private InstanceConverter lookForConverter(Class<?> getterType, Class<?> setterType) {
    InstanceConverter converter = null;
    // see if there's a converter
    try {
      converter = InstanceConverter.findConverter(getterType, setterType);
    }
    catch (ClassNotFoundException cnfe) {
      String msg = "No converter found from:" + getterType.getName() + " to:" + setterType.getName();
      _log.warn(msg, cnfe);
    }
    catch (InstantiationException ie) {
      String msg = "No converter found from:" + getterType.getName() + " to:" + setterType.getName();
      _log.warn(msg, ie);
    }
    catch (IllegalAccessException iae) {
      String msg = "No converter found from:" + getterType.getName() + " to:" + setterType.getName();
      _log.warn(msg, iae);
    }
    return converter;
  }

  /* (non-Javadoc)
   * @see java.lang.Comparable#compareTo(java.lang.Object)
   */
  @Override
  public int compareTo(FactoryMethodMapping fmo) {
    int ret = toString().compareTo(fmo.toString());
    return ret;
  }

  /* (non-Javadoc)
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof FactoryMethodMapping) {
      FactoryMethodMapping mapping = (FactoryMethodMapping) obj;
      return compareTo(mapping) == 0;
    }
    return super.equals(obj);
  }

  /* (non-Javadoc)
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode() {
    return factoryMethod.hashCode();
  }

  /* (non-Javadoc)
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();

    sb.append(factoryMethod.getReturnType().getName());
    sb.append(' ');
    sb.append(factoryMethod.getName());
    for(Class<?> clazz:factoryMethod.getParameterTypes()){
      sb.append(' ');
      sb.append(clazz.getName());
    }

    return sb.toString();
  }

  /**
   * copy the values from one instance to another using the internal mapping and
   * the converter if provided
   * @param from the instance being copied from
   * @param to the instance being copied to
   * @throws InvocationTargetException 
   * @throws IllegalAccessException 
   * @throws IllegalArgumentException 
   */
  public void copyValues(Object from, Object to) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException{
    Class<? extends Object> fromClass = from.getClass();
    for(Mapping mapping:mappings){
      Method getter = mapping.getGetter();
      if(getter.getDeclaringClass().isAssignableFrom(fromClass)){
        Object getValue = getter.invoke(from);
        Object convertedValue = mapping.converter == null?getValue:mapping.converter.convert(getValue);
        try {
          mapping.getSetter().invoke(to, convertedValue);
        }
        catch (IllegalArgumentException iae) {
          String msg = "Error invoking:" + mapping.getSetter().toString() + " with argument of class:" + convertedValue.getClass().getName();
          throw new IllegalArgumentException(msg, iae);
        }
      }
    }
  }

  /**
   * Build a unique key for this mapping
   * @return a unique key as a string
   */
  public String getKey(){
    return getKey(factoryMethod);
  }

  /**
   * Internal struct for storing a mapping
   * @author Thom Hehl
   */
  protected static class Mapping{
    private InstanceConverter
                      converter;
    private Class<?>  fromClass;
    private Method    getter;
    private Method    setter;

    /**
     * Create a mapping with no converter
     * @param fromClass the class the values will be copied from
     * @param getter the getter method
     * @param toClass the class the values will be copied from
     * @param setter the setter method
     */
    public Mapping(Class<?> fromClass, Method getter, Class<?> toClass, Method setter) {
      super();
      this.getter = getter;
      this.fromClass = fromClass;
      this.setter = setter;
    }

    /**
     * Create a mapping
     * @param fromClass the class the values will be copied from
     * @param getter the getter method
     * @param toClass the class the values will be copied from
     * @param setter the setter method
     * @param converter the converter to use to convert the value from the getter to the setter
     */
    public Mapping(Class<?> fromClass, Method getter, Class<?> toClass, Method setter, InstanceConverter converter) {
      this(fromClass, getter, toClass, setter);
      this.converter = converter;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
      return "Mapping [converter=" + converter + ", fromClass=" + fromClass + ", getter=" + getter + ", setter="
          + setter + "]";
    }

    /**
     * Get the converter used for this mapping
     * @return the converter
     */
    protected InstanceConverter getConverter(){
      return converter;
    }

    /**
     * @return the fromClass
     */
    public Class<?> getFromClass() {
      return fromClass;
    }

    /**
     * @return the getter
     */
    public final Method getGetter() {
      return getter;
    }

    /**
     * @return the setter
     */
    public final Method getSetter() {
      return setter;
    }
  }
}
